package org.g4studio.core.net.io;

import java.io.IOException;
import java.io.PushbackReader;
import java.io.Reader;

/**
 * DotTerminatedMessageReader is a class used to read messages from a server
 * that are terminated by a single dot followed by a &lt;CR&gt;&lt;LF&gt;
 * sequence and with double dots appearing at the begining of lines which do not
 * signal end of message yet start with a dot. Various Internet protocols such
 * as NNTP and POP3 produce messages of this type.
 * <p>
 * This class handles stripping of the duplicate period at the beginning of
 * lines starting with a period, converts NETASCII newlines to the local line
 * separator format, truncates the end of message indicator, and ensures you
 * cannot read past the end of the message.
 * 
 * @author <a href="mailto:savarese@apache.org">Daniel F. Savarese</a>
 * @version $Id: DotTerminatedMessageReader.java 165675 2005-05-02 20:09:55Z
 *          rwinston $
 */
public final class DotTerminatedMessageReader extends Reader {
	private static final String LS;
	private static final char[] LS_CHARS;

	static {
		LS = System.getProperty("line.separator");
		LS_CHARS = LS.toCharArray();
	}

	private boolean atBeginning;
	private boolean eof;
	private int pos;
	private char[] internalBuffer;
	private PushbackReader internalReader;

	/**
	 * Creates a DotTerminatedMessageReader that wraps an existing Reader input
	 * source.
	 * 
	 * @param reader
	 *            The Reader input source containing the message.
	 */
	public DotTerminatedMessageReader(Reader reader) {
		super(reader);
		internalBuffer = new char[LS_CHARS.length + 3];
		pos = internalBuffer.length;
		// Assumes input is at start of message
		atBeginning = true;
		eof = false;
		internalReader = new PushbackReader(reader);
	}

	/**
	 * Reads and returns the next character in the message. If the end of the
	 * message has been reached, returns -1. Note that a call to this method may
	 * result in multiple reads from the underlying input stream to decode the
	 * message properly (removing doubled dots and so on). All of this is
	 * transparent to the programmer and is only mentioned for completeness.
	 * 
	 * @return The next character in the message. Returns -1 if the end of the
	 *         message has been reached.
	 * @exception IOException
	 *                If an error occurs while reading the underlying stream.
	 */
	public int read() throws IOException {
		int ch;

		synchronized (lock) {
			if (pos < internalBuffer.length) {
				return internalBuffer[pos++];
			}

			if (eof) {
				return -1;
			}

			if ((ch = internalReader.read()) == -1) {
				eof = true;
				return -1;
			}

			if (atBeginning) {
				atBeginning = false;
				if (ch == '.') {
					ch = internalReader.read();

					if (ch != '.') {
						// read newline
						eof = true;
						internalReader.read();
						return -1;
					} else {
						return '.';
					}
				}
			}

			if (ch == '\r') {
				ch = internalReader.read();

				if (ch == '\n') {
					ch = internalReader.read();

					if (ch == '.') {
						ch = internalReader.read();

						if (ch != '.') {
							// read newline and indicate end of file
							internalReader.read();
							eof = true;
						} else {
							internalBuffer[--pos] = (char) ch;
						}
					} else {
						internalReader.unread(ch);
					}

					pos -= LS_CHARS.length;
					System.arraycopy(LS_CHARS, 0, internalBuffer, pos, LS_CHARS.length);
					ch = internalBuffer[pos++];
				} else {
					internalBuffer[--pos] = (char) ch;
					return '\r';
				}
			}

			return ch;
		}
	}

	/**
	 * Reads the next characters from the message into an array and returns the
	 * number of characters read. Returns -1 if the end of the message has been
	 * reached.
	 * 
	 * @param buffer
	 *            The character array in which to store the characters.
	 * @return The number of characters read. Returns -1 if the end of the
	 *         message has been reached.
	 * @exception IOException
	 *                If an error occurs in reading the underlying stream.
	 */
	public int read(char[] buffer) throws IOException {
		return read(buffer, 0, buffer.length);
	}

	/**
	 * Reads the next characters from the message into an array and returns the
	 * number of characters read. Returns -1 if the end of the message has been
	 * reached. The characters are stored in the array starting from the given
	 * offset and up to the length specified.
	 * 
	 * @param buffer
	 *            The character array in which to store the characters.
	 * @param offset
	 *            The offset into the array at which to start storing
	 *            characters.
	 * @param length
	 *            The number of characters to read.
	 * @return The number of characters read. Returns -1 if the end of the
	 *         message has been reached.
	 * @exception IOException
	 *                If an error occurs in reading the underlying stream.
	 */
	public int read(char[] buffer, int offset, int length) throws IOException {
		int ch, off;
		synchronized (lock) {
			if (length < 1) {
				return 0;
			}
			if ((ch = read()) == -1) {
				return -1;
			}
			off = offset;

			do {
				buffer[offset++] = (char) ch;
			} while (--length > 0 && (ch = read()) != -1);

			return (offset - off);
		}
	}

	/**
	 * Determines if the message is ready to be read.
	 * 
	 * @return True if the message is ready to be read, false if not.
	 * @exception IOException
	 *                If an error occurs while checking the underlying stream.
	 */
	public boolean ready() throws IOException {
		synchronized (lock) {
			return (pos < internalBuffer.length || internalReader.ready());
		}
	}

	/**
	 * Closes the message for reading. This doesn't actually close the
	 * underlying stream. The underlying stream may still be used for
	 * communicating with the server and therefore is not closed.
	 * <p>
	 * If the end of the message has not yet been reached, this method will read
	 * the remainder of the message until it reaches the end, so that the
	 * underlying stream may continue to be used properly for communicating with
	 * the server. If you do not fully read a message, you MUST close it,
	 * otherwise your program will likely hang or behave improperly.
	 * 
	 * @exception IOException
	 *                If an error occurs while reading the underlying stream.
	 */
	public void close() throws IOException {
		synchronized (lock) {
			if (internalReader == null) {
				return;
			}

			if (!eof) {
				while (read() != -1) {
					;
				}
			}
			eof = true;
			atBeginning = false;
			pos = internalBuffer.length;
			internalReader = null;
		}
	}
}
